package viper.main;

//import java.awt.event.ActionEvent;
//import java.awt.event.ActionListener;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//import javax.swing.JButton;
//import javax.swing.JLabel;
//import javax.swing.JPanel;

import viper.GUI.ViperGUI;
import viper.receive.CallReceiver;
import viper.receive.UDPReceiver;
import viper.send.CallSender;
import viper.send.UDPSender;

/**
 * @author Kevin Sin
 * 
 * Revision number: 2
 * Date created: 2/3/10
 * Date of Last Back-up on SVN: 2/3/10
 * 
 * <p>This class is the central class that keeps track of what a user is doing
 * - logged off, in a call, setting up a call, who the callee is...
 */
public class StateManager {
	//-----------------Singleton structure-----------------------------------
	public final static StateManager INSTANCE = new StateManager();
	private StateManager(){
		
	}
	
	public void initialize(){
		try{
			datagramSocket = new DatagramSocket(VIPER_PORT);
		} catch (SocketException e) {
			e.printStackTrace();
			return;
		}
	}
	//------------------------------------------------------------------------
	//----------------Represent state of the user-----------------------------
	public enum State {
		LOGGED_OUT, NOT_IN_CALL, DIALING, CALL_DECISION, IN_CALL 
	}
	private State state = State.LOGGED_OUT;
	
	private void setState(State state) {
		this.state = state;
	}

	public State getState() {
		return state;
	}
	//----------------------------------------------------------------
	//------------------------thread pool------------------------------
	private ExecutorService pool;
	
	/**
	 * @return pool
	 */
	public ExecutorService getPool() {
		return pool;
	}
	//----------------------------------------------------------------
	//------------------UDPSocket for receiving / sending------------
	private DatagramSocket datagramSocket; //UDP Socket
	
	/**
	 * @return datagramSocket
	 */
	public DatagramSocket getDatagramSocket() {
		return datagramSocket;
	}
	
	public static final int VIPER_PORT = 40052; //UDP port to use
	//-----------------------------------------------------------------
	//-----------------helper methods for IP addresses-----------------
	/**
	 * @return - IP address in String format
	 */
	public String getIP() {
		String ip = null;
		try {
			InetAddress address = InetAddress.getLocalHost();
			ip = address.getHostAddress();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
		return ip;
	}
	//------------------------------------------------------------------
	//-------other private variables and related methods----------------
	private String username; //name that is displayed
	private ArrayList<Object[]> peers; //peers online
	private ArrayList<Object[]> relays; //relays used in a call
	private Object[] calleeArray; //Object[] for callee	
	private byte diversityCode;

	/**
	 * @return
	 * Getter method for username
	 */
	public String getUsername() {
		return username;
	}
	
	/**
	 * @param username
	 * Setter method for username
	 */
	public void setUsername(String username) {
		this.username = username;
	}
	
	/**
	 * @return
	 * Getter method for peers
	 */
	public ArrayList<Object[]> getPeers() {
		return peers;
	}

	/**
	 * updates the peers variable
	 * make sure you are not added yourself
	 */
	public void updatePeers() {
		ArrayList<Object[]> users = MySQLFunctions.getCurrentUsers();
		for(Object[] userArray : users)
    	{
			if(((String)userArray[0]).equals(getIP()))
			{
				users.remove(userArray);
				break;
			}
    	}
		peers = users;
	}	
	
	public Object[] getPeerRowFromIP(String ip) {		
		for(Object[] row : peers)
		{
			if(((String)row[0]).equals(ip))
				return row;
		}
		return null;
	}
	
	/**
	 * @return
	 * Getter method for relays
	 */
	public ArrayList<Object[]> getRelays() {
		return relays;
	}

	/**
	 * @param
	 * Setter method for relays
	 */
	public void setRelays(ArrayList<Object[]> relays) {
		this.relays = relays;
	}

	/**
	 * @return
	 * Getter method for calleeArray
	 */
	public Object[] getCalleeArray() {
		return calleeArray;
	}

	/**
	 * @param calleeArray
	 * Setter method for calleeArray
	 */
	public void setCalleeArray(Object[] calleeArray) {
		this.calleeArray = calleeArray;
	}
	
	public byte getDiversityCode() {
		return diversityCode;
	}

	public void setDiversityCode(byte diversityCode) {
		this.diversityCode = diversityCode;
	}

	//------------------------------------------------------------
	//--------------------State transitions-----------------------
	/**
	 * @param username
	 * Transition from LOGGED_OUT to NOT_IN_CALL
	 */
	public void login(String username){
		setState(State.NOT_IN_CALL);
		
		pool = Executors.newCachedThreadPool();
		pool.execute(new UDPReceiver()); //start UDPReceiver
		
		setUsername(username);
		MySQLFunctions.login(getIP(), username);
		ViperGUI.INSTANCE.changeCard(ViperGUI.LOGGED_IN_CARD);
		
	}
	
	/**
	 * Transitions from NOT_IN_CALL to LOGGED_OUT
	 */
	public void logout(){
		MySQLFunctions.logout(getIP());
		setState(State.LOGGED_OUT);
		ViperGUI.INSTANCE.getInternalFrame().setVisible(false);
		ViperGUI.INSTANCE.changeCard(ViperGUI.LOGGED_OUT_CARD);
	}
	
//	/**
//	 * @param peerIndex which peer the user wants to talk to
//	 * Transition when setting up a call after picking somebody to call.
//	 */
//	public void setupCall(int peerIndex){
//		setCalleeArray(peers.get(peerIndex));
//		ViperGUI.INSTANCE.changeCard(ViperGUI.CALL_SENDER_SETUP_CARD);
//	}
	
	/**
	 * @param peerIP which peer the user wants to talk to
	 * Transition when setting up a call after picking somebody to call.
	 */
	public void setupCall(String peerIP){
		setCalleeArray(getPeerRowFromIP(peerIP));
		ViperGUI.INSTANCE.changeCard(ViperGUI.CALL_SENDER_SETUP_CARD);
	}

	/**
	 * @param sourceAddrIP
	 * @param typeCode
	 * @param relays
	 * 
	 * State transition from NOT_IN_CALL to CALL_DECISION
	 */
	public void gotCall(final String sourceAddrIP, final byte typeCode, final String[] relays) {
		if(state==State.IN_CALL)
		{
			rejectCall(sourceAddrIP, typeCode, relays);
			return;
		}
		setState(State.CALL_DECISION);

		//popup internal frame
//		ViperGUI.INSTANCE.getInternalFrame().setTitle(sourceAddrIP + 
//				" wants to talk to you!");
//		JPanel framePanel = ViperGUI.INSTANCE.getFramePanel();
//		framePanel.removeAll();
//		JButton acceptButton = new JButton("Accept");
//		acceptButton.addActionListener(new ActionListener() {
//			@Override
//			public void actionPerformed(ActionEvent e) {
//				acceptCall(sourceAddrIP, typeCode, relays);
//			}
//        });
//		JButton rejectButton = new JButton("Reject");
//		rejectButton.addActionListener(new ActionListener() {
//			@Override
//			public void actionPerformed(ActionEvent e) {
//				rejectCall(sourceAddrIP, typeCode, relays);
//			}
//        });
//		framePanel.add(acceptButton);
//		framePanel.add(rejectButton);
//		ViperGUI.INSTANCE.getInternalFrame().setVisible(true);
		ViperGUI.INSTANCE.changeInternalFrameCallDecision(sourceAddrIP, typeCode, relays);
	}
	
	/**
	 * State transition from CALL_DECISION to NOT_IN_CALL
	 */
	public void callCancelled()
	{
		if(state!=State.CALL_DECISION)
			return;
		setState(State.NOT_IN_CALL);
		ViperGUI.INSTANCE.getInternalFrame().setVisible(false);
	}
	
	/**
	 * State transition from CALL_DECISION to NOT_IN_CALL
	 */
	public void rejectCall(String sourceAddrIP, byte typeCode, String[] relays) {
		//send rejection packet back
		byte[] bytes = ViperHeader.makeCallSetupPacket(
				ViperHeader.SETUP_REJECT_CALL, 
				null);
		byte[] addr = UDPSender.ipStringToBytes(sourceAddrIP);
		UDPSender.sendUDP(addr, bytes);
		
		ViperGUI.INSTANCE.getInternalFrame().setVisible(false);
		
		ViperGUI.INSTANCE.changeCard(ViperGUI.LOGGED_IN_CARD);
		setState(State.NOT_IN_CALL);
	}

	/**
	 * State transition from CALL_DECISION to IN_CALL.
	 * Accept call, send OK packet back
	 */
	public void acceptCall(String sourceAddrIP, byte typeCode, String[] relayIPs){
		//send OK packet back
		byte[] bytes = ViperHeader.makeCallSetupPacket(
				ViperHeader.SETUP_ACCEPT_CALL, 
				null);
		byte[] addr = UDPSender.ipStringToBytes(sourceAddrIP);
		UDPSender.sendUDP(addr, bytes);
		
		//store typecode and relays and who is calling
		setDiversityCode(typeCode);
		ArrayList<Object[]> callRelays = new ArrayList<Object[]>();
		if(relayIPs!=null)
		{
			for (String relayIP : relayIPs)
			{
				callRelays.add(getPeerRowFromIP(relayIP));
			}
		}
		setRelays(callRelays);
		setCalleeArray(getPeerRowFromIP(sourceAddrIP));

		ViperGUI.INSTANCE.getInternalFrame().setVisible(false);
		
		pool.execute(new CallReceiver());
		ViperGUI.INSTANCE.changeCard(ViperGUI.IN_CALL_CARD);
		setState(State.IN_CALL);
	}
	
	/**
	 * State transition from NOT_IN_CALL to DIALING
	 */
	public void call(){
		setState(State.DIALING);
		
		//send setup packet
		byte[] payload = new byte[4*relays.size() + 1];
		payload[0] = diversityCode;
		int i = 0;
		for(Object[] relay : relays)
		{
			String ipString = (String)relay[0];
			byte[] ipBytes = UDPSender.ipStringToBytes(ipString);
			System.arraycopy(ipBytes, 0, payload, 4*i + 1, 4);
			i++;
		}
		
		byte[] bytes = ViperHeader.makeCallSetupPacket(
				ViperHeader.SETUP_MAKE_CALL, 
				payload
		);
		byte[] calleeAddrBytes = UDPSender.ipStringToBytes((String)calleeArray[0]);
		UDPSender.sendUDP(calleeAddrBytes, bytes);

		ViperGUI.INSTANCE.changeInternalFrameDialing();
	}
	
	/**
	 * State transition from DIALING to NOT_IN_CALL
	 */
	public void cancelCall(String sourceAddrIP){
		//send cancel call packet back
		byte[] bytes = ViperHeader.makeCallSetupPacket(
				ViperHeader.SETUP_CANCEL_CALL, 
				null);
		byte[] addr = UDPSender.ipStringToBytes(sourceAddrIP);
		UDPSender.sendUDP(addr, bytes);
		
		ViperGUI.INSTANCE.getInternalFrame().setVisible(false);
		ViperGUI.INSTANCE.changeCard(ViperGUI.LOGGED_IN_CARD);
		setState(State.NOT_IN_CALL);
	}
	
	/**
	 * State transition from DIALING to IN_CALL
	 */
	public void callAccepted() {
		if(state!=State.DIALING)
			return;
		pool.execute(new CallSender());
		ViperGUI.INSTANCE.changeCard(ViperGUI.IN_CALL_CARD);
		setState(State.IN_CALL);
		ViperGUI.INSTANCE.getInternalFrame().setVisible(false);
	}
	
	/**
	 * State transition from DIALING to NOT_IN_CALL
	 */
	public void callRejected() {
		if(state!=State.DIALING)
			return;
		ViperGUI.INSTANCE.changeCard(ViperGUI.LOGGED_IN_CARD);
		setState(State.NOT_IN_CALL);
		ViperGUI.INSTANCE.getInternalFrame().setVisible(false);
	}
	
	/**
	 * Transition from IN_CALL to NOT_IN_CALL
	 */
	public void endCall(){
		//send end call packet back
		byte[] bytes = ViperHeader.makeCallSetupPacket(
				ViperHeader.SETUP_END_CALL, 
				null);
		byte[] addr = UDPSender.ipStringToBytes((String)calleeArray[0]);
		UDPSender.sendUDP(addr, bytes);
		
		callEnded();
	}
	
	/**
	 * Used in transition from IN_CALL to NOT_IN_CALL
	 */
	public void callEnded(){
		if(state!=State.IN_CALL)
			return;
		setState(State.NOT_IN_CALL);
		ViperGUI.INSTANCE.changeCard(ViperGUI.LOGGED_IN_CARD);
	}
	//--------------------------------------------------------------
	//----------------------exiting the program---------------------
	/**
	 * called by ViperGUI -  when user closes the window,
	 * make sure the user gets logged out
	 */
	public void exit() {
		if(state!=State.LOGGED_OUT)
			logout();
	}
	//----------------------------------------------------------------
}
